package com.acme.gprs.server;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.net.Socket;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.Map;
import java.util.concurrent.Callable;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.FutureTask;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import java.util.logging.ConsoleHandler;
import java.util.logging.FileHandler;
import java.util.logging.Handler;
import java.util.logging.Level;
import java.util.logging.Logger;
import java.util.logging.SimpleFormatter;

/*import com.acme.gprs.alert.ws.Webservice;
import com.acme.gprs.alert.ws.WebservicePortType;*/
import com.acme.gprs.ws.Telecontrol;
import com.acme.gprs.ws.TelecontrolService;
import com.acme.utils.DateUtil;

final class GPRSApplianceClient implements Runnable{
	
	private class AsynchronousCommand implements Callable<GPRSMessage>{
		private int packageFlag;
		private String commandCode;
		private GPRSMessage rx;
		
		public AsynchronousCommand(int packageFlag, String commandCode) {
			this.packageFlag = packageFlag;
			this.commandCode = commandCode;
		}
		
		public synchronized void setRx(GPRSMessage rx){
			this.rx = rx;
			blockingCommandsMap.remove(packageFlag + "_" + commandCode);
			notify();
			dumpBlockingCommandsMap();
		}
		
		private synchronized GPRSMessage blockUntilRx() throws Exception{
			blockingCommandsMap.put(packageFlag + "_" + commandCode, this);
			dumpBlockingCommandsMap();
			wait();
			return this.rx;
		}
		
		@Override
		public synchronized GPRSMessage call() throws Exception {
			return blockUntilRx();
		}
		
		@Override
		public String toString() {
			return packageFlag + "_" + commandCode;
		}
	}
	
	
	private final static int EVENT_CODE_EXTERNAL_POWER_ON = 22;
	private final static int EVENT_CODE_EXTERNAL_POWER_OFF = 23;
	private final static int EVENT_CODE_OPEN_DOOR = 10;
	private final static int EVENT_CODE_CLOSE_DOOR = 2;
	private final static int EVENT_HEARTBEAT = 31;
	private final static int RESPONSE_CURRENT_LOCATION_REPORT = 34;
	private final static int RESPONSE_INTERVAL_REPORT = 35;
	
	private static Logger logger;
	private static DateFormat dateFormat = new SimpleDateFormat("dd/MM/yyyy HH:mm:ss");
	private GPRSAppliancesServer server;
	private GPRSApplianceClientId id;
	private Socket connection;
	private OutputStream out;
	private BufferedReader in;
	private int packageFlag = 'A';
	private Map<String,AsynchronousCommand> blockingCommandsMap = new ConcurrentHashMap<String,AsynchronousCommand>();
	
	static{
		//Configure the logger
		try {
			logger = Logger.getLogger(GPRSApplianceClient.class.getCanonicalName());
			Handler fileHandler = new FileHandler("gprs_clients.log");
			fileHandler.setFormatter(new SimpleFormatter());
			logger.addHandler(fileHandler);
			logger.addHandler(new ConsoleHandler());
			logger.setLevel(Level.FINEST);
		} catch (Exception e) {
			e.printStackTrace();
		}
	}
	
	GPRSApplianceClient(GPRSAppliancesServer server, GPRSApplianceClientId id, Socket connection){
		this.server = server;
		this.id = id;
		this.connection = connection;
		try {
			out = this.connection.getOutputStream();
			in = new BufferedReader(new InputStreamReader(connection.getInputStream()));
		} catch (IOException e) {
			e.printStackTrace();
		}
		Thread connectionThread = new Thread(this);
		connectionThread.start();
	}
	
	public GPRSApplianceClient(GPRSApplianceClientId id) {
		this.id = id;
	}

	public GPRSApplianceClientId getId() {
		return id;
	}
	
	void disconnect(){
		if(connection.isConnected()){
			try {
				connection.close();
			} catch (IOException e) {
				e.printStackTrace();
			}
		}
		server.removeGPRSApplianceConnection(this);
	}
	
	private char incrementPackageFlag(){
		if('z' == packageFlag){
			packageFlag = 'A';
		}else{
			packageFlag += 1;
		}
		return (char)packageFlag;
	}
	
	private void dumpBlockingCommandsMap(){
		logMessage("Blocking commands: ");
		for(String key : blockingCommandsMap.keySet()){
			logMessage("     > " + blockingCommandsMap.get(key).toString());
		}
	}
	
	private void setImei(GPRSMessage msg){
		if( id.getImei() == null || id.getImei().equals(GPRSApplianceClientId.IMEI_UNDEFINED) ){
			GPRSApplianceClient gprsApplianceClient = null;
			if( ( gprsApplianceClient = server.getGPRSApplianceByImei(msg.getImei() ) ) != null ){
				server.removeGPRSApplianceConnection(gprsApplianceClient);
			}
			id.setImei(msg.getImei());
			server.dumpClients();
		}else{
			id.setImei(msg.getImei());
		}
	}
	
	private void diggestEventMessage(String strEventMessage){
		//WebservicePortType webservicePortType = null;
		Telecontrol telecontrol = null;
		DateUtil dateUtil = null;
		String dateInMilis = null;
		String dateFormatted = null;
		try {
			dateUtil = new DateUtil();
			GPRSMessage msg = GPRSMessage.parseStrMessage(strEventMessage);
			if(msg != null){
				GPRSCommand command = GPRSCommand.getGPRSCommandByCode(msg.getCommandCode());
				if(GPRSCommand.AUTO_EVENT_REPORT.equals(command)){
					int eventCode = Integer.parseInt(msg.getData()[0]);
					dateInMilis = dateUtil.getDateInMilis(msg.getData()[3], DateUtil.YYMMDDHHMISS_FORMAT);
					dateFormatted = dateUtil.getDateFromMilis(dateInMilis, DateUtil.DDMMYYYYHHMMSS_FORMAT);
					switch(eventCode){
						case EVENT_CODE_EXTERNAL_POWER_ON:
							setImei(msg);
							/*webservicePortType = new Webservice().getWebservicePort();
							webservicePortType.createAlert(msg.getImei(), msg.getData()[3], GPRSMessage.ALERT_POWER_ON);*/
							telecontrol = new TelecontrolService().getTelecontrolPort();
							telecontrol.createAlert(msg.getImei(), dateInMilis, Integer.toString(GPRSMessage.ALERT_POWER_ON));
							logMessage("Alert[Power ON] date ["+dateInMilis+"] date-formatted ["+dateFormatted+"] imei["+msg.getImei()+"]");
							break;
						case EVENT_CODE_EXTERNAL_POWER_OFF:
							setImei(msg);
							/*webservicePortType = new Webservice().getWebservicePort();
							webservicePortType.createAlert(msg.getImei(), msg.getData()[3], GPRSMessage.ALERT_POWER_OFF);*/
							telecontrol = new TelecontrolService().getTelecontrolPort();
							telecontrol.createAlert(msg.getImei(), dateInMilis, Integer.toString(GPRSMessage.ALERT_POWER_OFF));
							logMessage("Alert[Power OFF] date ["+dateInMilis+"] date-formatted ["+dateFormatted+"] imei["+msg.getImei()+"]");
							break;
						case EVENT_CODE_OPEN_DOOR :
							setImei(msg);
							/*webservicePortType = new Webservice().getWebservicePort();
							webservicePortType.createAlert(msg.getImei(), msg.getData()[3], GPRSMessage.ALERT_OPEN_DOOR);*/
							telecontrol = new TelecontrolService().getTelecontrolPort();
							telecontrol.createAlert(msg.getImei(), dateInMilis, Integer.toString(GPRSMessage.ALERT_OPEN_DOOR));
							logMessage("Alert[Opened Door] date ["+dateInMilis+"] date-formatted ["+dateFormatted+"] imei["+msg.getImei()+"]");
							break;
						case EVENT_CODE_CLOSE_DOOR :
							setImei(msg);
							/*webservicePortType = new Webservice().getWebservicePort();
							webservicePortType.createAlert(msg.getImei(), msg.getData()[3], GPRSMessage.ALERT_CLOSE_DOOR);*/
							telecontrol = new TelecontrolService().getTelecontrolPort();
							telecontrol.createAlert(msg.getImei(), dateInMilis, Integer.toString(GPRSMessage.ALERT_CLOSE_DOOR));
							logMessage("Alert[Closed Door] date ["+dateInMilis+"] date-formatted ["+dateFormatted+"] imei["+msg.getImei()+"]");
							break;
						case RESPONSE_INTERVAL_REPORT:
							setImei(msg);
							break;
						case EVENT_HEARTBEAT:
							setImei(msg);
							break;
						case RESPONSE_CURRENT_LOCATION_REPORT:
							logMessage("Key received: " + msg.getPackageFlag()+"_"+command.getCode());
							AsynchronousCommand asyncCommand = blockingCommandsMap.get(msg.getPackageFlag()+"_A10");
							if(asyncCommand != null){
								asyncCommand.setRx(msg);
							}
							break;
					}
				}else if( GPRSCommand.OPEN_PORT.equals(command) || GPRSCommand.CLOSE_PORT.equals(command) || GPRSCommand.STATUS_PORT.equals(command) ){
					logMessage("Key received: " + msg.getPackageFlag()+"_"+command.getCode());
					AsynchronousCommand asyncCommand = blockingCommandsMap.get(msg.getPackageFlag()+"_"+command.getCode());
					if(asyncCommand != null){
						asyncCommand.setRx(msg);
					}
				}else{
					logMessage("diggestEventMessage: " + strEventMessage);
					//this.packageFlag = msg.getPackageFlag();
				}
			}
		} catch (Exception e) {
			e.printStackTrace();
		}
	}
	
	public GPRSMessage sendCommand(String command, String[] data) throws Exception, TimeoutException{
		GPRSMessage gprsRx = null;
		if(connection.isConnected()){
			GPRSMessage gprsTx = new GPRSMessage(incrementPackageFlag(), id.getImei(), command, data);
			logMessage("Sending command: " + gprsTx.toCanonicalRepr());
			out.write(gprsTx.toCanonicalRepr().getBytes());
			out.flush();
			
			FutureTask<GPRSMessage> asyncCommand = new FutureTask<GPRSMessage>(new AsynchronousCommand(gprsTx.getPackageFlag(), gprsTx.getCommandCode()));
			Thread t = new Thread(asyncCommand);
			t.start();
			try{
				gprsRx = asyncCommand.get(10, TimeUnit.SECONDS);
			}catch(Exception exception){
				blockingCommandsMap.remove(gprsTx.getPackageFlag() + "_" + gprsTx.getCommandCode());
				
				//gprsRx = sendCommand(command, data);
				
				out.write(gprsTx.toCanonicalRepr().getBytes());
				out.flush();
				asyncCommand = new FutureTask<GPRSMessage>(new AsynchronousCommand(gprsTx.getPackageFlag(), gprsTx.getCommandCode()));
				t = new Thread(asyncCommand);
				t.start();
				try{
					gprsRx = asyncCommand.get(20, TimeUnit.SECONDS);
				}catch(TimeoutException timeoutException){
					blockingCommandsMap.remove(gprsTx.getPackageFlag() + "_" + gprsTx.getCommandCode());
					timeoutException.printStackTrace();
					throw timeoutException;
				}catch(Exception nestedException){
					blockingCommandsMap.remove(gprsTx.getPackageFlag() + "_" + gprsTx.getCommandCode());
					nestedException.printStackTrace();
					throw nestedException;
				}
			}
			logMessage("RECEIVED COMMAND RESPONSE: " + gprsRx.toCanonicalRepr());
		}
		return gprsRx;
	}
	
	@Override
	public void run() {
		String incomingEventMsg;
		try{
			logMessage("Waiting event message from gprs: " + id.getHostAddress());
			while( (incomingEventMsg=in.readLine()) != null ){
				logMessage("RECEIVED EVENT: " + incomingEventMsg);
				diggestEventMessage(incomingEventMsg);
			}
		} catch (Exception e) {
			e.printStackTrace();
		}finally{
			disconnect();
		}
	}
	
	private static void logMessage(String msg){
		//logger.log(Level.INFO, dateFormat.format(new Date()) + " > " + msg);
		System.out.println(dateFormat.format(new Date()) + " (DEV) > " + msg);
	}
	
	@Override
	public String toString() {
		return id.toString();
	}
	
	@Override
	public int hashCode() {
		return id.hashCode();
	}
	
	@Override
	public boolean equals(Object obj) {
		boolean equals = false;
		if(obj!=null && obj instanceof GPRSApplianceClient){
			equals = id.getImei().equals(((GPRSApplianceClient)obj).getId().getImei()); 
		}
		return equals;
	}
}